var width = 800,
    height = 600;

var padding = 6;

var color = d3.scale.category20();
var radius = d3.scale.sqrt().range([2,16]);
var colorByGroup = function(node) { return color(node.group % 20); };

var svgBars, svgNodes, svgLinks;

var force = d3.layout.force()
    .charge(-50)
    .linkDistance(20)
    .size([ width, height ])
    .on("tick", function() {
      svgLinks
          .attr("x1", function(d) { return d.source.x; })
          .attr("y1", function(d) { return d.source.y; })
          .attr("x2", function(d) { return d.target.x; })
          .attr("y2", function(d) { return d.target.y; });
      svgNodes
          .each(collide(0.5))
          .attr("cx", function(d) { return d.x; })
          .attr("cy", function(d) { return d.y; });
    });

var svg = d3.select("#chart")
    .append("svg")
    .attr("width", width)
    .attr("height", height);

// Arrays of nodes and links as they were loaded from JSON data.
var unfilteredNodes, unfilteredLinks;

// Arrays of nodes and links after all filters have been applied.
var nodes, links;

// Arrays of files and owners parsed from nodes, where type 0 is an owner.
var owners, files;

// 2D array of group assignment labels, where assignments[0][5] is the label
// for node type 0, group 5. Populated after loading JSON data.
var assignments = [{}, {}];
var types = ["owner", "file"];

// TODO(alanv): These constants belong in a style sheet.
// Range buckets for owner nodes.
var RADIUS_RANGE = [5,10,15,20,25];
//Opacity used for points that fail pattern matching.
var NO_MATCH_OPACITY = 0.05;
//The default link opacity.
var LINK_OPACITY = 0.2;
var SELECTED_LINK_OPACITY = 0.5;
var LINK_COLOR = d3.rgb(100, 100, 100);
var SELECTED_LINK_COLOR = d3.rgb(50, 50, 50);
// The default node opacity.
var NODE_OPACITY = 0.75;
var SELECTED_NODE_OPACITY = 1.0;
var NODE_STROKE_COLOR = d3.rgb(255,255,255);
var SELECTED_NODE_STROKE_COLOR = d3.rgb(50,50,50);

var getParams = loadGetParams( {"source":"", "target":""} );

document.getElementById("sourceFilterInput").value = getParams.source;
document.getElementById("targetFilterInput").value = getParams.target;

var sourceMatcher = new RegExp(getParams.source);
var targetMatcher = new RegExp(getParams.target);

var testSourceMatcher = new RegExp("");
var testTargetMatcher = new RegExp("");

var currentSelectedNode = null;

function resetFilters() {
    sourceMatcher = new RegExp(getParams.source === null ? "" : getParams.source);
    targetMatcher = new RegExp(getParams.target === null ? "" : getParams.target);

    nodes = unfilteredNodes;
    links = unfilteredLinks;

    update();
}

function updateFilter(event) {
  var sourceInput = document.getElementById("sourceFilterInput");
  var targetInput = document.getElementById("targetFilterInput");

  testSourceMatcher = new RegExp(sourceInput.value, "i");
  testTargetMatcher = new RegExp(targetInput.value, "i");

  if (event.keyCode == 13) {
    applyFilters();
  } else {
    updateTestFilters();
  }
}

function applyFilters() {
  sourceMatcher = testSourceMatcher;
  targetMatcher = testTargetMatcher;

  update();
}

function setHighlight(node) {
  if (currentSelectedNode) {
    manageHighlightInternal(null, currentSelectedNode, 3, false);
  }
  currentSelectedNode = node;
  manageHighlightInternal(null, node, 3, true);
  drawSidebar(node);
  updateOpacity();
}

function manageHighlightInternal(previousNode, node, depth, enable) {
  if ((previousNode == node) || (depth == 0)) return;

  if (depth > 1) {
    node.links.map(function(link) {
      link.highlight = enable;
      if (enable) {
        d3.select(link.svg).moveToFront();
      } else {
        d3.select(link.svg).moveToBack();
      }
      manageHighlightInternal(node, link.target, (depth - 2), enable);
      manageHighlightInternal(node, link.source, (depth - 2), enable);
    });
  }

  node.highlight = enable;
  if (enable) {
    d3.select(node.svg).moveToFront();
  }
}

/**
 * TITLE
 */

svg.append("text")
    .attr("x", 20)
    .attr("y", height - 20)
    .attr("class", "title")
    .text("committers for frameworks/base");

/**
 * SIDEBAR
 */

var sidebarContainer = svg.append("g")
    .attr("transform", "translate(20,40)");
var sidebarLabel = sidebarContainer.append("text")
    .attr("id", "sidebar-label")
    .attr("class", "label")
    .attr("x", 0)
    .attr("y", 0)
    .text("hover over a node or bar for more information");

function drawSidebar(node) {
  var typeLabel = types[node.type];

  sidebarLabel
      .text(node.label + " (" + typeLabel + ", " + node.count + " lines)");

  // Sort nodes according to where they appear on-screen.
  var linkedNodes = node.links.map(function(link) {
    return [(link.source == node ? link.target : link.source), link.value];
  });

  var barHeight = 10;
  var x = d3.scale.linear()
      .range([5, 105])
      .domain([0, d3.max(linkedNodes, function(pair) { return pair[0].count; })]);

  var svgRects = sidebarContainer.selectAll("rect.count").data(linkedNodes);

  svgRects.exit()
      .remove();

  svgRects.enter()
      .append("rect")
      .attr("class", "count")
      .style("fill-opacity", 0.2)
      .style("stroke", NODE_STROKE_COLOR)
      .style("stroke-opacity", 0.5)
      .attr("height", barHeight)
      .attr("x", 0)
      .attr("y", function(d, i) { return (i * barHeight) + 15; });

  svgRects
      .attr("fill", function(d) { return colorByGroup(d[0]); })
      .attr("width", function(d) { return x(d[0].count); });

  var svgValue = sidebarContainer.selectAll("rect.value").data(linkedNodes);

  svgValue.exit()
      .remove();

  svgValue.enter()
      .append("rect")
      .attr("class", "value")
      .style("fill-opacity", 1.0)
      .style("stroke", NODE_STROKE_COLOR)
      .style("stroke-opacity", 0.5)
      .attr("height", barHeight)
      .attr("x", 0)
      .attr("y", function(d, i) { return (i * barHeight) + 15; });

  svgValue
      .attr("fill", function(d) { return colorByGroup(d[0]); })
      .attr("width", function(d) { return x(d[1]); });
}

var radiusFunction;
var radiusLegend = legend("lines owned", (width - 100), 40, 100, 200);

function updateRadiusLegend(range, domain) {
  var circle = radiusLegend.selectAll("circle.legend").data(range);

  circle.enter()
      .append("circle")
      .attr("class", "legend")
      .style("fill", colorByGroup(0))
      .style("fill-opacity", 0.2);

  circle.exit()
      .remove();

  var padding = {"x":5, "y":10};
  var xOffset = (d3.max(range) * 2);
  var yOffset = function (i) {
    return (i * padding.y) + (2 * d3.sum(range.slice(0,i))) + 20;
  };

  circle
      .attr("cx", function(d) { return (xOffset - d); })
      .attr("cy", function(d, i) { return yOffset(i); })
      .attr("r", function(d) { return d; });

  var text = radiusLegend.selectAll("text.legend").data(range);
  var segment = function(range, domain, i) {
    var left = Math.ceil((i / range.length) * domain[1] + domain[0]);
    var right = Math.floor(((i + 1) / range.length) * domain[1] + domain[0]);
    if (i == 0) return "< " + right;
    else if (i == (range.length - 1)) return "> " + left;
    else return "~ " + Math.round((left + right) / 2);
  };

  text.enter()
      .append("text")
      .attr("class", "legend");

  text.exit()
      .remove();

  text
      .attr("x", (xOffset + padding.x))
      .attr("y", function(d, i) { return yOffset(i) + 3; })
      .text(function (d, i) { return segment(range, domain, i); });
}

function legend(title, x, y, w, h) {
    var container = svg.append("g")
        .attr("transform", "translate(" + x + "," + y + ")");

    container.append("text")
        .attr("class", "legendTitle")
        .text(title)
        .attr("x", 0)
        .attr("y", 0);

    return container;
}

function updateTestFilters() {
  files.map(function(file) {
        file.testMatch = testTargetMatcher.test(file.label);
      });

  owners.map(function(owner) {
        owner.testMatch = testSourceMatcher.test(owner.label);
      });

  links.map(function(link) {
        link.testMatch = link.source.testMatch && link.target.testMatch;
      });

  updateOpacity();
}

var nodeOpacity = function(node) {
  return (node.highlight ? 0.6 : 0) + (node.testMatch ? 0.4 : 0) + 0.10;
};

var linkOpacity = function(link) {
  return (link.highlight ? 0.5 : 0) + (link.testMatch ? 0.2 : 0) + 0.05;
};

function updateOpacity() {
  svgBars
      .style("fill-opacity", nodeOpacity)
      .style("stroke-opacity", nodeOpacity);
  svgNodes
      .style("fill-opacity", nodeOpacity)
      .style("stroke-opacity", nodeOpacity);
  svgLinks
      .style("stroke-opacity", linkOpacity);
}

function update() {
  // Filter out nodes using matchers.
  nodes = nodes.filter(function(node) {
    node.keep = true;
    if (node.type == 0) {
      node.keep = (node.label.length > 0)
          && (node.label.indexOf('initial-contribution') < 0)
          && sourceMatcher.test(node.label);
    } else if (node.type == 1) {
      node.keep = targetMatcher.test(node.label);
    }
    return node.keep;
  });

  // Reset links.
  links.map(function(link) {
    link.highlight = false;
    link.testMatch = false;
  });

  // Reset node counts and links.
  nodes.map(function(node) {
    node.links = [];
    node.count = 0;
    node.highlight = false;
    node.testMatch = false;
  });

  // Filter out orphaned links.
  links = links.filter(function(link) {
    link.keep = (link.source.keep && link.target.keep);
    if (link.keep) {
        link.source.links.push(link);
        link.target.links.push(link);
    }
    return link.keep;
  });

  // Filter out orphaned nodes.
  nodes = nodes.filter(function(node) {
    node.keep = (node.links.length > 0);
    return node.keep;
  });

  // Separate owners and files.
  owners = [];
  files = [];

  nodes.map(function(node) {
    if (node.type == 0) {
      owners.push(node);
    } else {
      files.push(node);
    }
  });

  // Adjust force based on number of owners.
  force.linkDistance((width * 0.4) / Math.sqrt(files.length));

  console.log("has " + nodes.length + " nodes and " + links.length + " links");

  // Calculate maximum counts and assign links to nodes.
  links.map(function(link) {
     link.source.count += link.value;
     link.target.count += link.value;
  });

  // Set up radius buckets for owner nodes.
  var ownerDomain = [0, d3.max(owners, function(owner) { return owner.count; })];
  var ownerRadius = d3.scale.quantize()
      .range(RADIUS_RANGE)
      .domain(ownerDomain);

  updateRadiusLegend(RADIUS_RANGE, ownerDomain);

  owners.map(function(owner) { owner.radius = ownerRadius(owner.count); });

  // Draw legend for radius buckets.
  d3.select("text.legend_radius")
    .append("text")
    .

  /**
   * BARS
   * A bars represents an individual file. Bars are sorted by file name, which
   * means files within a specific folder are adjacent to one another. The
   * properties of a bar are mapped as:
   * - height => the number of lines in a file, relative to all currently
   *             visible files
   */

  // Sort files by label.
  files = files.sort(function (a, b) {
    return a.label > b.label ? 1 : -1;
  });

  var barAngle = (2.0 * Math.PI) / files.length;
  var barRadius = d3.scale.linear()
      .range([(width/2 - 200), (width/2 - 100)])
      .domain([0, d3.max(files, function(file) { return file.count; })]);
  var barWidth = Math.sin(barAngle) * barRadius(0);

  var bar = d3.svg.arc()
      .innerRadius(function(file) { return barRadius(0); })
      .outerRadius(function(file) { return 5 + barRadius(file.count); })
      .startAngle(function(file, i) { return (i * barAngle); })
      .endAngle(function(file, i) { return ((i + 1) * barAngle); });

  // Set files from data.
  svgBars = svg.selectAll("path.file")
      .data(files, function(file) { return file.unique_id; });

  // Remove old bars.
  svgBars.exit()
      .remove();

  // Set attributes for new bars.
  svgBars.enter()
      .append("path")
      .attr("class", "file")
      .each(function(file) { file.svg = this; })
      .style("fill", colorByGroup /* based on group */)
      .style("fill-opacity", NODE_OPACITY)
      .style("stroke", NODE_STROKE_COLOR)
      .style("stroke-opacity", NODE_OPACITY)
      .style("stroke-width", 1)
      .attr("transform", "translate("+(width/2)+","+(height/2)+")")
      .on("mouseover", setHighlight);

  // Update dynamic bar properties.
  svgBars
      .attr("d", bar)
      .each(function(file, i) {
        var angle = ((i + 0.5) * barAngle) - (Math.PI / 2.0);
        var radius = barRadius(0);
        file.x = file.px = (width / 2.0) + (Math.cos(angle) * radius);
        file.y = file.py = (height / 2.0) + (Math.sin(angle) * radius);
        file.fixed = true;
      });

  /**
   * LINKS
   * A link represents an aggregate contribution from an owner to a file, or
   * the number of lines "owner" within a file. Its properties are mapped as:
   * - stroke width => the size of a contribution, relative to all currently
   *                   visible contributions
   */

  var maxLinkValue = d3.max(links, function(link) { return link.value; });

  // Set links from data.
  svgLinks = svg.selectAll("line.link")
      .data(links, function(link) { return link.unique_id; });

  // Remove old links.
  svgLinks.exit()
      .remove();

  // Set attributes for new links.
  svgLinks.enter()
      .append("line")
      .each(function (link) { link.svg = this; })
      .attr("class", "link")
      .style("stroke", LINK_COLOR)
      .style("stroke-opacity", LINK_OPACITY);

  // Update dynamic link properties.
  svgLinks
      .style("stroke-width", function(link) {
        // TODO: Taper width to fit the owner node.
        return 1 + (barWidth - 1) * (link.value / maxLinkValue);
      });

  /**
   * NODES
   * A node represents an individual contributor based on their unique email
   * address. Nodes are arranged via force-directed layout and thus tend to
   * appear close to groups of files that they have modified. The properties of
   * a node are mapped as:
   * - radius => the total number of lines contributed to the set of visible
   *             files, relative to those of all visible contributors
   */

  // Set nodes from data.
  svgNodes = svg.selectAll("circle.node")
      .data(owners, function(node) { return node.unique_id; });

  // Remove old nodes.
  svgNodes.exit()
      .remove();

  // Set attributes for new nodes.
  svgNodes.enter()
      .append("circle")
      .each(function (node) { node.svg = this; })
      .attr("class", "node")
      .style("fill", colorByGroup)
      .style("fill-opacity", NODE_OPACITY)
      .style("stroke", NODE_STROKE_COLOR)
      .style("stroke-opacity", NODE_OPACITY)
      .style("stroke-width", 2)
      .on("mouseover", setHighlight)
      .call(force.drag);

  // Update dynamic node properties.
  svgNodes
      .attr("r", function(node) { return ownerRadius(node.count); });

  force.nodes(nodes)
      .links(links)
      .start();

  updateTestFilters();
}

var assignGroups = function(nodes) {
  var groups = [];

  var getFragment = [
      function(label) { return label.substr(label.indexOf('@') + 1); },
      function(label) { return label.substr(0, label.lastIndexOf('/')); }
  ];

  // Extract fragments
  nodes.map(function(node) {
    var fragment = getFragment[node.type](node.label);
    if (typeof groups[node.type] == "undefined") {
      groups[node.type] = new Object();
    }
    if (typeof groups[node.type][fragment] == "undefined") {
      groups[node.type][fragment] = new Array();
    }
    groups[node.type][fragment].push(node);
  });

  groups.map(function (group, type) {
    var i = 0;
    for (fragment in group) {
      group[fragment].map(function (node) {
        node.group = i;
      });
      assignments[type][i] = fragment;
      i++;
    }
  });
};

d3.json("filtered_data.json", function(json) {
  nodes = unfilteredNodes = json.nodes;
  links = unfilteredLinks = json.links;

  assignGroups(nodes);

  unfilteredNodes.map(function (node, index) {
    node.unique_id = index;
    node.totalLines = 0;
  });

  unfilteredLinks.map(function (link, index) {
    link.unique_id = index;
    link.source = nodes[link.source];
    link.target = nodes[link.target];
    link.source.totalLines += link.value;
    link.target.totalLines += link.value;
  });

  update();
});
